package org.zxt.web.nio;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.zip.GZIPOutputStream;

public class Response implements Closeable {

    /**
     * HTTP status code after processing, e.g. "200 OK", Status.OK
     */
    protected IStatus status;

    /**
     * MIME type of content, e.g. "text/html"
     */
    protected String mimeType;

    /**
     * Data of the response, may be null.
     */
    protected BufferedReader ous;

    protected long contentLength;

    @SuppressWarnings("serial")
    protected final Map<String, String> header = new HashMap<String, String>() {

        public String put(String key, String value) {
        lowerCaseHeader.put(key == null ? key : key.toLowerCase(), value);
        return super.put(key, value);
        }

        ;
    };

    /**
     * copy of the header map with all the keys lowercase for faster
     * searching.
     */
    private final Map<String, String> lowerCaseHeader = new HashMap<String, String>();

    /**
     * The request method that spawned this response.
     */
    private Method requestMethod;

    /**
     * Use chunkedTransfer
     */
    private boolean chunkedTransfer;

    private boolean encodeAsGzip;

    private boolean keepAlive;
    IHTTPSession session;

    /**
     * Creates a fixed length response if totalBytes>=0, otherwise chunked.
     */
    public Response(IHTTPSession session, IStatus status, String mimeType, String data) {
        this.status = status;
        this.mimeType = mimeType;
        this.session = session;
        this.requestMethod = session.getMethod();
        if (data == null||data.trim().isEmpty()) {
            this.contentLength = 0L;
        } else {
            byte[] bs = data.getBytes();
            this.contentLength = bs.length;
            this.ous = new BufferedReader(new StringReader(data));
        }
        this.chunkedTransfer = this.contentLength <= 0;
        keepAlive = true;
    }
    /**
     * Creates a fixed length response if totalBytes>=0, otherwise chunked.
     */
    public Response(IHTTPSession session, IStatus status, String mimeType, BufferedReader br, long len) throws IOException {
        this.status = status;
        this.mimeType = mimeType;
        this.session = session;
        this.requestMethod = session.getMethod();
        this.ous = br;
        this.contentLength = len;
        this.chunkedTransfer = this.contentLength <= 0;
        keepAlive = true;
    }

    @Override
    public void close() throws IOException {
        if (this.ous != null) {
            this.ous.close();
        }
    }

    /**
     * Adds given line to the header.
     */
    public void addHeader(String name, String value) {
        this.header.put(name, value);
    }

    /**
     * Indicate to close the connection after the Response has been sent.
     *
     * @param close {@code true} to hint connection closing, {@code false} to
     *              let connection be closed by client.
     */
    public void closeConnection(boolean close) {
        if (close)
            this.header.put("connection", "close");
        else
            this.header.remove("connection");
    }

    /**
     * @return {@code true} if connection is to be closed after this
     * Response has been sent.
     */
    public boolean isCloseConnection() {
        return "close".equals(getHeader("connection"));
    }

    public BufferedReader getData() {
        return this.ous;
    }

    public String getHeader(String name) {
        return this.lowerCaseHeader.get(name.toLowerCase());
    }

    public String getMimeType() {
        return this.mimeType;
    }

    public Method getRequestMethod() {
        return this.requestMethod;
    }

    public int getStatus() {
        return this.status.getRequestStatus();
    }

    public Response setGzipEncoding(boolean encodeAsGzip) {
        this.encodeAsGzip = encodeAsGzip;
        return this;
    }

    public void setKeepAlive(boolean useKeepAlive) {
        this.keepAlive = useKeepAlive;
    }

    /**
     * Sends given response to the socket.
     */
    protected void send() throws IOException {
        SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
        gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
        if(this.ous == null){
            this.chunkedTransfer = false;
//            this.status = Status.NO_CONTENT;
        }
        if (this.status == null) {
            throw new Error("sendResponse(): Status can't be null.");
        }
        BufferedOutputStream pw = new BufferedOutputStream(getSocketChannel());
//        PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, new ContentType(this.mimeType).getEncoding())), false);
        pw.write(("HTTP/1.1 "+this.status.getDescription()+" \r\n").getBytes());
        if (this.mimeType != null) {
//            outputStream.write(("Content-Type:"+this.mimeType).getBytes());
            printHeader(pw, "Content-Type", this.mimeType);
        }
        if (getHeader("date") == null) {
//            outputStream.write(("Date:"+gmtFrmt.format(new Date())+"\n").getBytes());
            printHeader(pw, "Date", gmtFrmt.format(new Date()));
        }
        printHeader(pw, "Accept-Ranges", "bytes");
//        printHeader(pw, "Content-Security-Policy", "default-src * 'self'");
//        printHeader(pw, "Access-Control-Allow-Headers", "*");
//        printHeader(pw, "Access-Control-Allow-Origin", "*");
//        printHeader(pw, "Access-Control-Allow-Method", "*");
        printHeader(pw, "Cache-Control", "max-age=31536000");
        printHeader(pw, "Cache-Control", "public");
        printHeader(pw, "Server", "httpd");
//        printHeader(pw, "Cross-Origin-Resource-Policy", "cross-origin");
        for (Map.Entry<String, String> entry : this.header.entrySet()) {
            printHeader(pw, entry.getKey(), entry.getValue());
        }
        if (getHeader("connection") == null) {
            printHeader(pw, "Connection", (this.keepAlive ? "keep-alive" : "close"));
        }
//        encodeAsGzip = true;
//        if (getHeader("content-length") != null) {
//            encodeAsGzip = false;
//        }
        if (encodeAsGzip) {
            printHeader(pw, "Content-Encoding", "gzip");
            setChunkedTransfer(true);
        }
        long pending = this.contentLength;//this.getData() != null ? this.contentLength : 0;
        if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
            printHeader(pw, "Transfer-Encoding", "chunked");
        } else if (!encodeAsGzip) {
            pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, pending);
        }
        pw.write("\r\n".getBytes());
//        pw.println("");
        pw.flush();
        sendBodyWithCorrectTransferAndEncoding(pw, pending);
        pw.flush();
//        this.getSocketChannel().socket().getOutputStream().flush();
//        this.getSocketChannel().shutdownOutput();
        NioHTTPD.safeClose(this.getData());

        NioHTTPD.safeClose(pw);
    }

    protected SocketChannel getSocketChannel(){
        return this.session.getSocketChanel();
    }

    @SuppressWarnings("static-method")
    protected void printHeader(BufferedOutputStream pw, String key, String value) throws IOException {
        pw.write((key+": "+value+"\r\n").getBytes());
    }

    protected long sendContentLengthHeaderIfNotAlreadyPresent(BufferedOutputStream pw, long defaultSize) throws IOException {
        String contentLengthString = getHeader("content-length");
        long size = defaultSize;
        if (contentLengthString != null) {
            try {
                size = Long.parseLong(contentLengthString);
            } catch (NumberFormatException ex) {
                NioHTTPD.LOG.severe("content-length was no number " + contentLengthString);
            }
        }
        printHeader(pw,"Content-Length", String.valueOf(size));
//        pw.print("Content-Length: " + size + "\r\n");
        return size;
    }

    private void sendBodyWithCorrectTransferAndEncoding(BufferedOutputStream outputStream, long pending) throws IOException {
        if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
            ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream);
            sendBodyWithCorrectEncoding(chunkedOutputStream, pending);
            chunkedOutputStream.finish();
        } else {
            sendBodyWithCorrectEncoding(outputStream, pending);
        }
    }

    private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException {
        if (encodeAsGzip) {
            GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
            sendBody(gzipOutputStream, -1, encodeAsGzip);
            gzipOutputStream.finish();
        } else {
            sendBody(outputStream, pending, encodeAsGzip);
        }
    }

    public void sendBody(OutputStream outputStream, long pending, boolean encodeAsGzip) throws IOException {
        char[] bs = new char[2*1024];
        int len = 0;
        boolean sendEverything = pending == -1;
        while (pending > 0 || sendEverything){
            len = this.getData().read(bs);
            if(pending == -1 && len == -1){
                break;
            }
            if(len != -1){
                byte[] bbs = String.valueOf(bs, 0, len).getBytes(StandardCharsets.UTF_8);
                outputStream.write(bbs);
                if (pending > 0) {
                    pending -= bbs.length;
                }
                if(pending == 0){
                    outputStream.flush();
                    return;
                }
            } else {
                System.out.println("read empty! hode on");
                return;
            }
        }
    }

    public void setChunkedTransfer(boolean chunkedTransfer) {
        this.chunkedTransfer = chunkedTransfer;
    }

    public void setData(BufferedReader ins) {
        this.ous = ins;
    }

    public void setMimeType(String mimeType) {
        this.mimeType = mimeType;
    }

    public void setRequestMethod(Method requestMethod) {
        this.requestMethod = requestMethod;
    }

    public void setStatus(IStatus status) {
        this.status = status;
    }
}
